Detección de Sonrisa en Tiempo Real con OpenCV

Conceptos de procesamiento de imágenes

Ph.D. Pablo Eduardo Caicedo-Rodríguez

Objetivo

  • Entender el flujo del procesamiento de imágenes: webcam → preproceso → detección rostro → ROI → detección sonrisa → overlay
  • Extraer los conceptos de procesamiento de imágenes que aparecen en cada paso
  • Aprender a ajustar parámetros para controlar: sensibilidad, falsos positivos y rendimiento

Contexto: implementación clásica y rápida (Haar cascades) para tiempo real.

Archivo: taller.py

El script en una frase

  • Captura frames de la webcam
  • Convierte a grises y mejora contraste con ecualización de histograma
  • Detecta rostros con un clasificador en cascada
  • En cada rostro (ROI) detecta sonrisas
  • Dibuja rectángulos y escribe texto en el frame

Agenda

  1. Pipeline de visión en tiempo real
  2. Imagen digital como matriz + espacios de color
  3. Preprocesamiento: grises + ecualización
  4. Detección Viola–Jones / Haar Cascades
  5. Parámetros detectMultiScale: trade-offs
  6. ROI y detección jerárquica
  7. Anotación: bounding boxes y texto
  8. Fallos típicos, buenas prácticas y extensiones

Pipeline típico de visión por computador

  1. Adquisición: sensor/cámara → frame
  2. Preprocesamiento: normalización/contraste/ruido
  3. Detección: localizar objetos (rostro)
  4. Análisis local: sub-detección (sonrisa en la ROI)
  5. Decisión: regla simple (hay detección vs no)
  6. Visualización: overlays en el frame

Adquisición de imágenes

Definición

La adquisición de imágenes es el paso en el que capturas una imagen usando un dispositivo, para poder guardarla o analizarla después.

Características

Quién captura: una cámara (celular, webcam), un escáner, o un equipo médico (RX, TAC, RM).

Qué obtienes: una imagen digital, que es una cuadrícula de punticos llamados píxeles.

Si es video: no es una sola imagen, sino muchas imágenes seguidas. Cada una se llama frame (cuadro).

Adquisición de imágenes

Ejemplo sencillo:

Cuando abres la cámara del celular y tomas una foto, eso es adquisición. Si grabas un video, estás adquiriendo muchos frames por segundo.

1A) Adquisición: sensor/cámara → frame (una “foto”)

  • Una cámara no “ve” continuo: toma muchas fotos por segundo.
  • Cada foto se llama frame (cuadro).
  • Un video es una secuencia de frames.
  • Ejemplo: 30 fps = 30 frames por segundo.

Idea clave: Primero capturamos una imagen. Luego la analizamos.

1B) Adquisición: ¿qué puede salir mal?

  • Luz baja → frame oscuro.
  • Movimiento → frame borroso.
  • Resolución baja → menos detalles.
  • Muchos frames/segundo → más “suave”, pero cuesta más procesar.

Regla simple: mejor iluminación + cámara estable = mejor punto de partida.

2A) Preprocesamiento: “arreglar” el frame para que sea más fácil

  • Es como limpiar unas gafas antes de leer.
  • Objetivo: que lo importante se vea más claro.
  • Cosas típicas:
    • Normalización: poner valores en un rango “parecido”.
    • Contraste: separar mejor claro/oscuro.
    • Ruido: quitar “granito” o puntos raros.

2B) Preprocesamiento: ejemplos sencillos

  • Normalizar: hacer que la imagen quede entre 0 y 1.
  • Aumentar contraste: que la cara se diferencie del fondo.
  • Suavizar ruido: un “filtro” que promedia vecinos.

Ojo: si suavizas demasiado, puedes borrar detalles (como la boca).

2B) Preprocesamiento: ejemplos sencillos

3A) Detección: localizar objetos (por ejemplo, un rostro)

  • “Detectar” = responder: ¿dónde está el objeto?
  • Resultado típico:
    • Un rectángulo (caja) alrededor del rostro.
    • Un nivel de confianza (qué tan seguro está el sistema).

Ejemplo: “Encontré 1 cara en (x, y, ancho, alto) con 0.92 de confianza”.

3B) Detección: cómo pensarla (sin entrar en cosas difíciles)

  • La computadora busca patrones que se parezcan a “rostro”.
  • Puede usar:
    • Métodos clásicos (formas, bordes).
    • Métodos modernos (modelos entrenados con muchas fotos).

Idea clave: Detectar no es “entender”, es “ubicar”.

3B) Detección: cómo pensarla (sin entrar en cosas difíciles)

4A) Análisis local: sub-detección dentro de una ROI

  • ROI = Región de Interés (una parte del frame).
  • Si ya encontraste el rostro, puedes mirar solo una parte:
    • La boca para detectar sonrisa.
  • Ventaja: menos área → más rápido y más simple.

4B) Análisis local: ejemplo “sonrisa en la boca”

  1. Detectas el rostro (caja grande).
  2. Recortas la ROI de la boca (caja pequeña).
  3. Ejecutas un “mini detector” de sonrisa.
  4. Sale un número: smile_score (0 a 1).

Idea clave: primero grande (rostro), luego pequeño (boca).

5A) Decisión: regla simple (hay detección vs no)

  • Aquí el sistema decide una salida final.
  • Ejemplo de regla muy simple:
    • Si confianza del rostro ≥ 0.6 → “hay rostro”.
    • Si no → “no hay rostro”.

Esto es una regla, no magia. A veces se equivoca.

5B) Decisión: ejemplo con “rostro + sonrisa”

  • Reglas de ejemplo:
    • Si rostro ≥ 0.6 y sonrisa ≥ 0.7 → “Sonrisa detectada”
    • Si rostro ≥ 0.6 y sonrisa < 0.7 → “Rostro sin sonrisa”
    • Si rostro < 0.6 → “Sin rostro”

Palabras útiles: - Falso positivo = detecta algo que no era. - Falso negativo = no detecta algo que sí estaba.

6A) Visualización: overlays (dibujos encima del frame)

  • Overlays = “capas” dibujadas sobre la imagen:
    • Rectángulos alrededor de objetos.
    • Puntos (ojos, nariz).
    • Texto (“Rostro: 0.92”, “Sonrisa: sí”).
  • Sirve para que un humano entienda rápido qué vio el sistema.

6B) Visualización: mini ejemplo (pseudo-código)

# frame: imagen original
# face_box: (x, y, w, h)
# score_face: 0.92

draw_rectangle(frame, face_box)
draw_text(frame, f"Rostro {score_face:.2f}", position=(x, y-10))

# si también hay sonrisa:
# draw_text(frame, "Sonrisa: SI", position=(x, y+h+20))
show(frame)

Tiempo real: ¿por qué importa?

  • En tiempo real importan dos métricas:
    • Latencia: cuánto tarda en procesar un frame
    • FPS: cuántos frames por segundo se sostienen

La cascada Haar es popular porque es rápida en CPU, aunque no sea SOTA.

Imagen digital: la idea mínima

  • Una imagen es una matriz:
    • 2D: (alto, ancho) si es escala de grises
    • 3D: (alto, ancho, canales) si es color

En OpenCV, un frame de webcam suele ser uint8 en rango 0–255 por canal.

Espacios de color: BGR vs Gray

  • OpenCV usa BGR (no RGB) por defecto
  • El detector en cascada opera sobre una banda (grises)
  • Por eso aparece:

gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

Conversión a grises: ¿qué se “pierde”?

  • Se reduce información cromática
  • Se conserva principalmente estructura: bordes, sombras, texturas
  • Para Haar/Viola–Jones, eso es suficiente:
    • busca patrones de contraste (claro/oscuro)

Preprocesamiento: ¿por qué tocar el contraste?

Problema: en webcam real hay - cambios de iluminación - sombras parciales - auto-exposure

Solución simple del script: - gray = cv2.equalizeHist(gray)

Histograma de intensidades (recordatorio)

  • El histograma cuenta cuántos píxeles hay en cada nivel de gris
  • Si la imagen está “oscura”, el histograma se concentra a la izquierda
  • Si está “lavada”, se concentra a la derecha o en un rango estrecho

La ecualización intenta “expandir” ese rango.

Ecualización de histograma: intuición

  • Transforma la imagen para que el histograma sea más uniforme
  • Efecto:
    • aumenta contraste global
    • puede mejorar detectabilidad de rasgos
    • también puede amplificar ruido en zonas oscuras

En el script se usa como robustez general.

¿Qué alternativa moderna usarías?

Para discusión: - CLAHE (ecualización adaptativa con límite de contraste) - Normalización por iluminación / homomorphic filtering - Modelos CNN con entrenamiento robusto

Pero aquí el objetivo es entender una cadena clásica, clara y rápida.

Detección: ¿qué es “detectar” aquí?

  • Entrada: imagen (grises)
  • Salida: lista de rectángulos:
    • cada rectángulo es un bounding box: (x, y, w, h)

En el código: - faces = face_cascade.detectMultiScale(...)

Viola–Jones / Haar Cascades (visión general)

Tres ideas clave:

  1. Características Haar: patrones rectangulares de contraste
  2. Imagen integral: cálculo ultrarrápido de sumas en rectángulos
  3. Cascada: etapas de clasificación que rechazan rápido la mayoría de ventanas

Por eso funciona bien en tiempo real en CPU.

Características Haar (intuición)

  • No son bordes “bonitos”; son tests tipo:
    • “¿esta región es más clara que esta otra?”
  • Un rostro tiene regularidades:
    • zona ojos más oscura que mejillas
    • puente nasal, etc.

Una sonrisa también genera contrastes (boca/dientes/labios).

Imagen integral (por qué acelera)

  • Permite obtener la suma de intensidades de cualquier rectángulo con pocas operaciones
  • Esto hace viable evaluar miles de ventanas por frame

No lo implementas tú: OpenCV lo hace internamente.

Cascada: la estrategia

  • Etapas tempranas: baratas y estrictas → rechazan rápido
  • Etapas tardías: más costosas → solo para candidatos

Resultado: - menor carga computacional - pero depende mucho del dataset de entrenamiento del XML

Ventana deslizante + escalas

  • El detector “mueve” una ventana por la imagen
  • Repite el proceso en múltiples escalas (para detectar objetos grandes/pequeños)

Ahí entra scaleFactor.

detectMultiScale: parámetros clave

Para rostros:

faces = self.face_cascade.detectMultiScale(
    gray, scaleFactor=1.3, minNeighbors=5
)

Para sonrisa (en ROI):

smiles = self.smile_cascade.detectMultiScale(
    roi_gray, scaleFactor=1.5, minNeighbors=32, minSize=(25, 25)
)

scaleFactor: precisión vs velocidad

  • scaleFactor = 1.3:
    • más niveles de escala (que 1.5)
    • mejor cobertura de tamaños
    • más costo computacional
  • scaleFactor = 1.5 (sonrisa):
    • menos escalas → más rápido
    • puede perder sonrisas pequeñas

minNeighbors: filtro de falsos positivos

  • Cuántas detecciones “consistentes” se necesitan para aceptar una
  • Bajo (p.ej. 3–5): más sensible, más falsos positivos
  • Alto (p.ej. 20–40): más estricto, menos falsos positivos

El script pone 32 para sonrisa: bastante estricto.

minSize: evitar basura

  • Define tamaño mínimo de objeto a aceptar
  • Para sonrisa:
    • minSize=(25,25) evita detectar texturas minúsculas como “sonrisa”

En la práctica: - subes minSize si hay muchos falsos positivos pequeños - bajas minSize si te pierdes sonrisas en gente lejos de la cámara

Concepto central: ROI (Región de Interés)

Se detecta en dos niveles:

  1. Rostro en toda la imagen
  2. Sonrisa solo dentro del rostro

Código: - roi_gray = gray[y:y+h, x:x+w] :

Beneficios: - menos cómputo - menos falsos positivos - coherencia semántica: la sonrisa “vive” en el rostro

ROI y coordenadas: cuidado mental

  • frame (global) tiene coordenadas (x, y)
  • roi_color tiene coordenadas locales (sx, sy)
  • Por eso el rectángulo de sonrisa se dibuja sobre roi_color:
  • cv2.rectangle(roi_color, (sx,sy), (sx+sw, sy+sh), ...)

Bounding boxes: representación y dibujo

  • Un bounding box es un rectángulo definido por:
    • esquina superior-izquierda (x, y)
    • ancho w, alto h

Dibujo: - cv2.rectangle(frame, (x,y), (x+w, y+h), ...)

Overlay de texto: “estado” del sistema

El script implementa una regla:

  • si len(smiles) > 0 → “Sonrisa Detectada”
  • si no → “No detectada / Serio”

Esto es postprocesamiento mínimo y una decisión binaria simple.

Decisión binaria: ¿qué significa realmente?

  • “Sonrisa detectada” ≠ emoción real
  • Solo significa:
    • “se encontraron patrones compatibles con el clasificador”

Importante para evitar sobreinterpretación, sobre todo en contextos biomédicos.

Bucles de adquisición: control de flujo

  • cap = cv2.VideoCapture(0) abre la webcam
  • ret, frame = cap.read() obtiene un frame
  • cv2.imshow(...) muestra la salida
  • cv2.waitKey(1) permite refresco y captura tecla
  • if ... == ord("q") sale del loop

Robustez: validaciones del script

  • Verifica que los XML cargaron:
    • if self.face_cascade.empty() or self.smile_cascade.empty(): raise ... :content
  • Verifica acceso a webcam:
    • if not cap.isOpened(): ... return

Concepto de ingeniería: fallar temprano con errores claros.

Rendimiento: ¿dónde se gasta el tiempo?

  • Mayor costo: detectMultiScale
    • porque evalúa muchas ventanas por frame
  • Reducir costo:
    • trabajar con ROI
    • subir scaleFactor (menos escalas)
    • subir minSize
    • bajar resolución del frame antes de detectar

Mini-experimento mental (5 min)

Si hay demasiados falsos positivos de sonrisa: - sube minNeighbors (ya está alto: 32) - sube minSize - reduce ecualización o usa CLAHE - restringe ROI a zona inferior del rostro (boca) en vez de todo el rostro

Mini-experimento mental (5 min)

Si no detecta sonrisas en gente lejos: - baja minSize (p.ej. 15x15) - baja scaleFactor (p.ej. 1.3) - baja minNeighbors (p.ej. 15–25) - aumenta iluminación o mejora exposición

Trade-off inevitable: sensibilidad vs falsos positivos.

Errores típicos (y por qué pasan)

  • Pose (cara girada): cascada frontal falla
  • Oclusión (mano, mascarilla): pierde rasgos
  • Luz dura (contraluz): cambia contrastes → se rompe Haar
  • Resolución baja: rasgos poco definidos
  • Expresiones sutiles: patrón no coincide con entrenamiento

¿Qué “conceptos” debes dominar para entender el script?

Checklist:

  • Imagen como matriz, tipo de dato uint8
  • Espacios de color (BGR) y conversión a grises
  • Histograma y ecualización
  • Detección de objetos vs clasificación
  • Bounding boxes y coordenadas
  • ROI (recorte) y jerarquía de detección
  • Viola–Jones: Haar features, imagen integral, cascada
  • Parámetros detectMultiScale y trade-offs
  • Visualización en tiempo real (imshow, waitKey)

Todo aparece en el flujo del código.

Actividad sugerida (para estudiantes)

  1. Ejecutar el script tal cual
  2. Cambiar un parámetro a la vez (registrar):
    • scaleFactor (rostro)
    • minNeighbors (rostro)
    • minNeighbors (sonrisa)
    • minSize (sonrisa)
  3. Documentar:
    • FPS aproximado (sensación)
    • falsos positivos
    • falsos negativos

Extensión 1: mejorar contraste sin amplificar tanto ruido

  • Sustituir equalizeHist por CLAHE
  • Comparar resultados con iluminación variable

(Útil en escenarios biomédicos con condiciones de luz no controladas)

Extensión 2: segmentar “zona boca” dentro del rostro

  • Dividir el rostro en regiones:
    • parte inferior ~ 40%–55% del alto
  • Detectar sonrisa solo allí:
    • menos falsos positivos
    • mejor rendimiento

Extensión 3: migrar a métodos modernos (discusión)

  • Detección facial: DNN (OpenCV DNN), MediaPipe, YOLO-face
  • Sonrisa/expresión: clasificación CNN en landmarks o en recortes

Pero: - mayor complejidad - más dependencias - potencial necesidad de GPU